package cn.edu.ctgu.geo.util;

import cn.edu.ctgu.geo.constants.GridData;
import cn.edu.ctgu.geo.type.*;

import java.util.ArrayList;
import java.util.List;

import static cn.edu.ctgu.geo.Codec2D.*;

public class LocationCodeUtils {
    /**
     * 获取某一处网格的大致网格大小(只是估计值)
     * @param code 网格码
     * @param level 层级
     * @return 包含经度方向和纬度方向网格大小的数组
     */
    public static double[] getGridSize(String code, int level) {
        code = shorten(code, level);
        LngLat lngLat = decode(code);

        double cosLat = Math.cos(Math.toRadians(Math.abs(lngLat.getLatDegree())));
        double lngLength = (GridData.gridSizes1[level][0] / 3600) * 111.7 * cosLat;
        double latLength = (GridData.gridSizes1[level][1] / 3600) * 111.7;

        return new double[]{lngLength, latLength};
    }
    /**
     * 缩短一个北斗二维网格编码
     * @param code 北斗二维网格编码
     * @param level 目标层级
     * @return 缩短后的编码
     */
    public static String shorten(String code, int level) {
        // level=0时只返回半球编号
        if (level < 0 || level > 10) {
            throw new IllegalArgumentException("层级错误");
        }
        int nowLevel = getCodeLevel(code);
        if (nowLevel <= level) {
            return code;
        }
        return code.substring(0, GridData.codeLengthAtLevel[level]);
    }
    /**
     * 获取一个位置码的最大级别
     * @param code 位置码
     * @return 级别
     */
    public static int getCodeLevel(String code) {
        int length = code.length();
        int level = -1;
        // 查找 code.length() 在 codeLengthAtLevel 数组中的索引
        for (int i = 0; i < GridData.codeLengthAtLevel.length; i++) {
            if (GridData.codeLengthAtLevel[i] == length) {
                level = i;
                break;
            }
        }
        if (level == -1) {
            throw new IllegalArgumentException("编码长度错误!");
        }
        return level;
    }
    /**
     * 获取位置码的半球信息：东南、东北、西南、西北
     * @param code 位置码
     * @return 包含经度方向和纬度方向的数组
     */
    public static Direction getDirections(String code) {
        // 判断纬度方向
        LatDirection latDir = code.charAt(0) == 'N' ? LatDirection.N : LatDirection.S;
        // 判断经度方向
        int lngValue = Integer.parseInt(code.substring(1, 3));
        LngDirection lngDir = lngValue >= 31 ? LngDirection.E : LngDirection.W;
        return new Direction(lngDir, latDir);
    }

    /**
     * 根据半球方向信息获取符号
     * @param directions 包含经度方向和纬度方向的数组
     * @return 包含经度符号和纬度符号的数组
     */
    public static int[] getSigns(Direction directions) {
        int lngSign = directions.getLngDirection() == LngDirection.E ? 1 : -1;
        int latSign = directions.getLatDirection() == LatDirection.N ? 1 : -1;
        return new int[]{lngSign, latSign};
    }
    /**
     * 检查第 level 级代码片段范围是否合法
     * @param lng 列号
     * @param lat 行号
     * @param level 级别
     */
    private static void checkCodeFragmentRange(int lng, int lat, int level) {
        if (lng > GridData.gridCount1[level][0] - 1
                || lng < 0
                || lat < 0
                || lat > GridData.gridCount1[level][1] - 1) {
            throw new IllegalArgumentException("位置码错误");
        }
    }
    /**
     * 获取某一级别的网格的行列号
     * @param codeFragment 某级别位置码片段
     * @param level 级别
     * @return 包含列号和行号的数组
     */
    public static int[] getRowAndCol(String codeFragment, int level) {
        if (codeFragment.length() != GridData.codeLengthAtLevel[level] - GridData.codeLengthAtLevel[level - 1]) {
            throw new IllegalArgumentException("编码片段长度错误!");
        }
        int lng;
        int lat;
        switch (level) {
            case 0:
                return new int[]{0, 0};
            case 1:
                lng = Integer.parseInt(codeFragment.substring(0, 2));
                lat = codeFragment.charAt(2) - 65;
                break;
            case 2:
            case 4:
            case 5:
            case 7:
            case 8:
            case 9:
            case 10:
                lng = Integer.parseInt(String.valueOf(codeFragment.charAt(0)), 16);
                lat = Integer.parseInt(String.valueOf(codeFragment.charAt(1)), 16);
                break;
            case 3:
            case 6:
                int n = Integer.parseInt(codeFragment);
                lng = n % 2;
                lat = (n - lng) / 2;
                break;
            default:
                throw new IllegalArgumentException("层级错误!");
        }
        checkCodeFragmentRange(lng, lat, level);
        return new int[]{lng, lat};
    }

    /**
     * 获取某一级别的代码片段
     * @param code 位置码
     * @param level 级别
     * @return 该级别的位置码片段
     */
    public static String getCodeAtLevel(String code, int level) {
        if (level == 0) {
            return String.valueOf(code.charAt(0));
        }
        return code.substring(GridData.codeLengthAtLevel[level - 1], GridData.codeLengthAtLevel[level]);
    }
    /**
     * 用于计算两个同级网格之间相差多少格，注意此方法不同于北斗参照网格码算法
     *
     * @param reference 被参考位置网格码
     * @param target    目标位置网格码
     * @return [lngDiff, latDiff]，经纬度方向分别偏差网格数量(按照半球的坐标轴方向)
     */
    public static int[] getOffset(String reference, String target) {
        int level = getCodeLevel(reference);
        target = shorten(target, level);
        // 如果level-1层的网格相同，直接进行减法即可
        if (reference.substring(0, GridData.codeLengthAtLevel[level - 1]).equals(target.substring(0, GridData.codeLengthAtLevel[level - 1]))) {
            int[] rRowCol = getRowAndCol(getCodeAtLevel(reference, level), level);
            int[] tRowCol = getRowAndCol(getCodeAtLevel(target, level), level);
            return new int[]{tRowCol[0] - rRowCol[0], tRowCol[1] - rRowCol[1]};
        } else {
            // 如果level-1层不同，为了计算简单，转为经纬度计算
            // 获取目标位置和参考位置的坐标(用度分秒保证计算误差)
            LngLat tLngLat = decode(target, new DecodeOption("dms"));
            double[] tInSecond = getSecond(tLngLat);
            LngLat rLngLat = decode(reference, new DecodeOption("dms"));
            double[] rInSecond = getSecond(rLngLat);
            // 获取半球信息
            Direction directions = getDirections(reference);
            int[] signs = getSigns(directions);
            // 乘上符号是为了变换到东北半球计算
            // 东北半球网格的原点在左下角(西南角)，对于参考坐标系的负方向(西、南)方向取整需要补1，所以直接使用Math.floor
            // 列差
            double lngDiff = ((tInSecond[0] - rInSecond[0]) / GridData.gridSizes1[level][0]) * signs[0];
            lngDiff = Math.floor(lngDiff);
            // 行差
            double latDiff = ((tInSecond[1] - rInSecond[1]) / GridData.gridSizes1[level][1]) * signs[1];
            latDiff = Math.floor(latDiff);
            return new int[]{(int) lngDiff, (int) latDiff};
        }
    }

    /**
     *
     * @param code    被参考的网格码
     * @param offsetX 经度方向偏移格数(按照半球的坐标轴方向)
     * @param offsetY 纬度方向偏移格数(按照半球的坐标轴方向)
     * @return 相对位置的网格码
     */
    public static String getRelativeGrid(String code, int offsetX, int offsetY) {
        int level = getCodeLevel(code);
        int[] rowCol = getRowAndCol(getCodeAtLevel(code, level), level);
        int newX = rowCol[0] + offsetX;
        int newY = rowCol[1] + offsetY;
        if (newX >= 0 && newX < GridData.gridCount1[level][0] && newY >= 0 && newY < GridData.gridCount1[level][1]) {
            // 如果两个网格的上一层网格相同可以直接相加得到结果
            return code.substring(0, GridData.codeLengthAtLevel[level - 1]) + encodeFragment(level, newX, newY);
        } else {
            // 上一层网格不相同，采用经纬度计算
            // 采用度分秒可以避免计算误差
            LngLat lngLat = decode(code, new DecodeOption("dms"));
            // 半球符号lngSign与latSign各自约去(平方为1)
            lngLat.setLngSecond(lngLat.getLngSecond()+ offsetX * GridData.gridSizes1[level][0]);
            lngLat.setLatSecond(lngLat.getLatSecond()+ offsetY * GridData.gridSizes1[level][1]);
            return encode(lngLat, level);
        }
    }
    /**
     * 同一级位于同一个父网格下的两个网格，获取之间的所有网格
     *
     * @param start 起始网格
     * @param end   结束网格
     * @return 所有网格
     */
    public static List<String> getAmongUs(String start, String end) {
        int levelStart = getCodeLevel(start);
        int levelEnd = getCodeLevel(end);
        if (levelStart != levelEnd ||!shorten(start, levelStart - 1).equals(shorten(end, levelEnd - 1))) {
            throw new IllegalArgumentException("两个编码必须等级相同且处于同一个上层网格内");
        }
        // 获取行和列
        int[] startRowCol = getRowAndCol(getCodeAtLevel(start, levelStart), levelStart);
        int[] endRowCol = getRowAndCol(getCodeAtLevel(end, levelEnd), levelEnd);
        int startX = Math.min(startRowCol[0], endRowCol[0]);
        int endX = Math.max(startRowCol[0], endRowCol[0]);
        int startY = Math.min(startRowCol[1], endRowCol[1]);
        int endY = Math.max(startRowCol[1], endRowCol[1]);
        String prefix = shorten(start, levelStart - 1);
        List<String> results = new ArrayList<>();
        for (int i = startX; i <= endX; i++) {
            for (int j = startY; j <= endY; j++) {
                results.add(prefix + encodeFragment(levelStart, i, j));
            }
        }
        return results;
    }
    /**
     * 获取一个网格相邻网格码，默认是周围九个(包括自己)
     *
     * @param code    目标网格码
     * @param offsets 需要获取的网格码的偏移量(按照半球的坐标轴方向)
     * @return 相邻网格码列表
     */
    public static List<String> getNeighbors(String code, int[][] offsets) {
        if (offsets == null || offsets.length == 0) {
            // 默认的偏移量，对应周围九个网格（包括自身）
            offsets = new int[][]{
                    {-1, -1},
                    {-1, 0},
                    {-1, 1},
                    {0, -1},
                    {0, 0},
                    {0, 1},
                    {1, -1},
                    {1, 0},
                    {1, 1}
            };
        }
        List<String> neighbors = new ArrayList<>();
        for (int[] offset : offsets) {
            neighbors.add(getRelativeGrid(code, offset[0], offset[1]));
        }
        return neighbors;
    }

    /**
     *
     * @param target    目标区域位置
     * @param reference 参考网格位置码
     * @return 参考网格位置码
     */
    public static String refer(String target, String reference) {
        return refer(target, reference, "-");
    }
    /**
     *
     * @param target    目标区域位置
     * @param reference 参考网格位置码
     * @param separator 分隔符
     * @return 参考网格位置码
     */
    public static String refer(String target, String reference, String separator) {
        if (separator == null) {
            separator = "-";
        }

        int level = getCodeLevel(reference);
        if (level < 5) {
            // 因为第五级有15个网格，而参考码网格最多有8个
            throw new IllegalArgumentException("参照网格编码必须大于等于5级");
        }
        // 获取半球信息
        Direction directions = getDirections(reference);
        int[] signs = getSigns(directions);
        int lngSign = signs[0];
        int latSign = signs[1];
        int[] diff = getOffset(reference, target);
        int lngDiff = diff[0] * lngSign;
        int latDiff = diff[1] * latSign;
        if (Math.abs(lngDiff) > 7 || Math.abs(latDiff) > 7) {
            throw new IllegalArgumentException("不可进行参考");
        }
        StringBuilder c = new StringBuilder(reference).append(separator);
        // 对第level进行参照
        if (lngDiff >= 0) {
            c.append(lngDiff);
        } else {
            c.append((char) (64 - lngDiff));
        }
        if (latDiff >= 0) {
            c.append(latDiff);
        } else {
            c.append((char) (64 - latDiff));
        }
        int tLevel = getCodeLevel(target);
        // 对剩余的层级进行参照
        for (int i = level + 1; i <= tLevel; i++) {
            // a为列号，b为行号
            int[] rowCol = getRowAndCol(getCodeAtLevel(target, i), i);
            int a = rowCol[0];
            int b = rowCol[1];
            c.append(separator);
            // 如果符号为负，需要取字母
            if (lngSign == 1 || a == 0) {
                c.append(a);
            } else {
                c.append((char) (64 + a));
            }
            if (latSign == 1 || b == 0) {
                c.append(b);
            } else {
                c.append((char) (64 + b));
            }
        }
        return c.toString();
    }
    /**
     * 还原斗参考网格位置码
     *
     * @param code      北斗参考网格位置码
     * @return 还原后的北斗参考网格位置码
     */
    public static String deRefer(String code) {
        return deRefer(code, "-");
    }
    /**
     * 还原斗参考网格位置码
     *
     * @param code      北斗参考网格位置码
     * @param separator 分隔符，默认是 "-"
     * @return 还原后的北斗参考网格位置码
     */
    public static String deRefer(String code, String separator) {
        if (separator == null) {
            separator = "-";
        }
        String[] split = code.split(separator);
        if (split.length == 1) {
            return code;
        }
        // 参考位置网格等级
        int rLevel = getCodeLevel(split[0]);
        // 目标位置网格等级
        int tLevel = rLevel + split.length - 2;
        int[] signs = getSigns(getDirections(split[0]));
        int lngSign = signs[0];
        int latSign = signs[1];

        // 获取编码的 ascii 码范围
        int ascii_0 = '0';
        int ascii_7 = '7';
        int ascii_A = 'A';
        int ascii_G = 'G';
        StringBuilder result = new StringBuilder();
        for (int i = rLevel; i <= tLevel; i++) {
            int offsetX, offsetY;
            char charX = split[1 + i - rLevel].charAt(0);
            char charY = split[1 + i - rLevel].charAt(1);
            // 计算经度方向偏移位置
            if (charX >= ascii_0 && charX <= ascii_7) {
                offsetX = (charX - ascii_0) * lngSign;
            } else if (charX >= ascii_A && charX <= ascii_G) {
                offsetX = -((charX - ascii_A + 1) * lngSign);
            } else {
                throw new IllegalArgumentException("参照码错误, 必须在 0~7、A~G 之间");
            }
            // 计算纬度方向偏移位置
            if (charY >= ascii_0 && charY <= ascii_7) {
                offsetY = (charY - ascii_0) * latSign;
            } else if (charY >= ascii_A && charY <= ascii_G) {
                offsetY = -((charY - ascii_A + 1) * latSign);
            } else {
                throw new IllegalArgumentException("参照码错误, 必须在 0~7、A~G 之间");
            }
            if (i == rLevel) {
                // 对 level 级进行还原
                result.append(getRelativeGrid(split[0], offsetX, offsetY));
            } else {
                result.append(encodeFragment(i, offsetX, offsetY));
            }
        }
        return result.toString();
    }
    /**
     * 获取一个参照位置网格的可参照范围
     *
     * @param code 参照位置网格编码，必须大于等于 5 级
     * @return [LngLat, LngLat]，西南角和东北角坐标
     */
    public static LngLat[] getReferRange(String code) {
        int level = getCodeLevel(code);
        if (level < 5) {
            throw new IllegalArgumentException("参照网格编码必须大于等于 5 级");
        }
        LngLat lngLat = decode(code, new DecodeOption("dms"));
        double[] lngLatInSecond = getSecond(lngLat);
        double westBound;
        double eastBound;
        double northBound;
        double southBound;
        // 乘数因子为 8 的项需要减掉一个第十级网格大小，是因为边界上并不能参照
        if (lngLatInSecond[0] >= 0) {
            westBound = lngLatInSecond[0] - 7 * GridData.gridSizes1[level][0];
            eastBound = lngLatInSecond[0] + 8 * GridData.gridSizes1[level][0] - GridData.gridSizes1[10][0];
        } else {
            westBound = lngLatInSecond[0] - 8 * GridData.gridSizes1[level][0] + GridData.gridSizes1[10][0];
            eastBound = lngLatInSecond[0] + 7 * GridData.gridSizes1[level][0];
        }
        if (lngLatInSecond[1] >= 0) {
            southBound = lngLatInSecond[1] - 7 * GridData.gridSizes1[level][1];
            northBound = lngLatInSecond[1] + 8 * GridData.gridSizes1[level][1] - GridData.gridSizes1[10][1];
        } else {
            southBound = lngLatInSecond[1] - 8 * GridData.gridSizes1[level][1] + GridData.gridSizes1[10][1];
            northBound = lngLatInSecond[1] + 7 * GridData.gridSizes1[level][1];
        }
        return new LngLat[]{
                new LngLat(westBound / 3600.0, southBound / 3600.0),
                new LngLat(eastBound / 3600.0, northBound / 3600.0)
        };
    }

    public static void main(String[] args) {
        String code = "N50J47539B8255346152";
        System.out.println("code:"+code);
        Direction directions = getDirections(code);
        int[] signs = getSigns(directions);
        System.out.println("经度方向: " + directions.getLngDirection());
        System.out.println("纬度方向: " + directions.getLatDirection());
        System.out.println("经度符号: " + signs[0]);
        System.out.println("纬度符号: " + signs[1]);

        try {
            int[] result = getRowAndCol("02C", 1);
            System.out.println("列号: " + result[0] + ", 行号: " + result[1]);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }

        System.out.println("codeLevel:"+getCodeAtLevel(code,4));

        // shorten 路径
        System.out.println("shortenCode:");
        for (int level = 2; level < 11; ++level) {
            System.out.println(level+":"+shorten(code,level));
        }

        int level = 10;
        double[] gridSize = getGridSize(code, level);
        System.out.println("经度方向网格大小: " + gridSize[0]);
        System.out.println("纬度方向网格大小: " + gridSize[1]);

        String reference = "N50J47539B82553461";
        System.out.println("参照 " + reference);
        String referNE1 = refer(code, reference);
        System.out.println(referNE1);
        System.out.println("还原");
        String deReferNE1 = deRefer(referNE1);
        System.out.println(deReferNE1);

        // 第一次参照和还原操作
        System.out.println("-------------------");
        System.out.println("参照 N50J47539b82");
        String codeNE = code;
        String referNE2 = refer(codeNE, "N50J47539b82");
        System.out.println(referNE2);
        System.out.println("还原");
        String deReferNE2 = deRefer(referNE2);
        System.out.println(deReferNE2);

        // 第二次参照和还原操作
        System.out.println("-------------------");
        System.out.println("参照 N50J4754909");
        String referNE3 = refer(codeNE, "N50J4754909");
        System.out.println(referNE3);
        System.out.println("还原");
        String deReferNE3 = deRefer(referNE3);
        System.out.println(deReferNE3);

        System.out.println("---------------------------------------");
        // 缩短北斗二维网格位置码
        String codeToBeShorten = "N50J47539B8255346152";
        System.out.println("需要缩短的编码: " + codeToBeShorten);

        for (int i = 9; i >= 2; i--) {
            System.out.println("缩短到第" + i + "级的编码是: " + shorten(codeToBeShorten, i));
        }

        System.out.println("---------------------------------------");
        LngLat lngLatSW = new LngLat(116, 18, 45.37, LngDirection.W, 39, 59, 35.38, LatDirection.S);
        System.out.println("坐标: " + lngLatSW);
        String codeSW = encode(lngLatSW, 10);
        System.out.println("北斗二维网格位置码: " + codeSW);
        System.out.println("解码 => " + decode(codeSW));
        System.out.println("-------------------");
        System.out.println("参照 S11J47539B82553461");
        String referSW1 = refer(codeSW, "S11J47539B82553461");
        System.out.println(referSW1);
        System.out.println("还原");
        String deReferSW1 = deRefer(referSW1);
        System.out.println(deReferSW1);
        System.out.println("-------------------");
        System.out.println("参照 S11J47539b82");
        String referSW2 = refer(codeSW, "S11J47539b82");
        System.out.println(referSW2);
        System.out.println("还原");
        String deReferSW2 = deRefer(referSW2);
        System.out.println(deReferSW2);
        System.out.println("-------------------");
        System.out.println("参照 S11J4754909");
        String referSW3 = refer(codeSW, "S11J4754909");
        System.out.println(referSW3);
        System.out.println("还原");
        String deReferSW3 = deRefer(referSW3);
        System.out.println(deReferSW3);

        System.out.println("---------------------------------------");
        System.out.println("参考特殊样例1:");
        String codeTarget1 = "N50J47539b82";
        String codeReference1 = "N50J47539b8";
        System.out.println(codeTarget1 + " 参考 " + codeReference1);
        String codeRefer1 = refer(codeTarget1, codeReference1);
        System.out.println(codeRefer1);
        System.out.println("-------------------");
        System.out.println("参考特殊样例2:");
        String codeTarget2 = "S50J4750908";
        String codeReference2 = "S50J474E9E8";
        System.out.println(codeTarget2 + " 参考 " + codeReference2);
        String codeRefer2 = refer(codeTarget2, codeReference2);
        System.out.println(codeRefer2);
    }

}
